#ifndef _SQLITE_PREPARED_STATEMENT_H_
#define _SQLITE_PREPARED_STATEMENT_H_

#include "sqlite3.h"

#include "Badge.h"
#include "Blob.h"
#include "Row.h"

#include <iostream>
#include <string>
#include <type_traits>

namespace sqlite
{

class Database;

class PreparedStatement
{
    /// Possible states in which the prepared statement may be found
    enum class State
    {
        NotReady,
        Ready,
        FirstRow,
        InProgress,
        Error
    };

public:
    /// Constructs the statement with a given query string. This may only
    /// be called by the \ref Database class . PreparedStatements are
    /// generated by calling Database.prepare(..)
    PreparedStatement(Badge<Database>, sqlite3 *db, const std::string &sql);

    /// Constructs the statement with a given query string, in raw form with the
    /// number of bytes specified. This may only be called by the \ref Database class 
    /// PreparedStatements are generated by calling Database.prepare(..)
    PreparedStatement(Badge<Database>, sqlite3 *db, const char *sql, int nByte);

    /// Frees the resources that were associated with the statement
    ~PreparedStatement();

    /**
     * @brief Executes the statement - this may be invoked for
     *        any type of statement (SELECT, UPDATE, INSERT, CREATE,
     *        etc.)
     * @return True on successful execution, false otherwise.
     */
    bool execute();

    /**
     * @brief Attempts to fetch the next row in the result set of a
     *        SELECT statement. execute() may be called before this
     *        method, after a reset(), or not at all. The next() method
     *        will determine the correct action to perform based on the
     *        statement's internal state.
     * @return True on success, false otherwise.
     */
    bool next();

    /// Resets the statement into a "ready to execute" state, clearing
    /// any bound parameters in the process.
    void reset();

    template<class T>
    void bind(int index, const T &value, bool copyData = false)
    {
        if (m_handle == nullptr
                || index < 0
                || index >= SQLITE_LIMIT_VARIABLE_NUMBER)
            return;

        // SQLite indices begin with 1, while our API uses 0-offset indices
        index++;

        using paramType = typename std::decay<T>::type;

        if constexpr (std::is_same_v<char*, paramType> || std::is_same_v<const char*, paramType>)
        {
            auto bindingType = copyData ? SQLITE_TRANSIENT : SQLITE_STATIC;
            sqlite3_bind_text(m_handle, index, value, -1, bindingType);
        }
        else if constexpr (std::is_same_v<std::string, paramType>)
        {
            auto bindingType = copyData ? SQLITE_TRANSIENT : SQLITE_STATIC;
            sqlite3_bind_text(m_handle, index, value.data(), value.size(), bindingType);
        }
        else if constexpr (std::is_same_v<Blob, paramType>)
        {
            auto bindingType = copyData ? SQLITE_TRANSIENT : SQLITE_STATIC;
            sqlite3_bind_blob(m_handle, index, value.data.data(), value.data.size(), bindingType);
        }
        else if constexpr (std::is_integral_v<paramType>)
        {
            sqlite3_bind_int64(m_handle, index, static_cast<sqlite3_int64>(value));
        }
        else if constexpr (std::is_floating_point_v<paramType>)
        {
            sqlite3_bind_double(m_handle, index, static_cast<double>(value));
        }
        else if constexpr (std::is_base_of_v<Row, T>)
        {
            value.marshal(*this);
        }
    }

    // Attempts to read the given value as a bind parameter, while internally keeping
    // track of the column index
    template<class T>
    void read(const T &input, bool copyData = false)
    {
        bind(m_colIdx, input, copyData);

        if constexpr (!std::is_base_of_v<Row, T>)
        {
            m_colIdx++;
        }
    }

    // Attempts to read the value of the current (row,column) into the output type
    template <class T>
    void write(T &output)
    {
        if (m_colIdx >= m_numCols)
            return;

        using paramType = typename std::decay<T>::type;
        if constexpr (std::is_same_v<std::string, paramType>)
        {
            if (sqlite3_column_type(m_handle, m_colIdx) == SQLITE_NULL)
            {
                output = std::string();
            }
            else
            {
                const char *data = reinterpret_cast<const char*>(sqlite3_column_text(m_handle, m_colIdx));
                int numBytes = sqlite3_column_bytes(m_handle, m_colIdx);

                output = std::string(data, numBytes);
            }
            
            m_colIdx++;
        }
        else if constexpr (std::is_same_v<Blob, paramType>)
        {
            if (sqlite3_column_type(m_handle, m_colIdx) == SQLITE_NULL)
            {
                output.data = std::string();
            }
            else
            {
                const char *data = reinterpret_cast<const char*>(sqlite3_column_text(m_handle, m_colIdx));
                int numBytes = sqlite3_column_bytes(m_handle, m_colIdx);

                output.data = std::string(data, numBytes);
            }

            m_colIdx++;
        }
        else if constexpr (std::is_integral_v<paramType>)
        {
            if (sqlite3_column_type(m_handle, m_colIdx) == SQLITE_NULL)
                output = 0;
            else
                output = sqlite3_column_int64(m_handle, m_colIdx);

            m_colIdx++;
        }
        else if constexpr (std::is_floating_point_v<paramType>)
        {
            if (sqlite3_column_type(m_handle, m_colIdx) == SQLITE_NULL)
                output = static_cast<T>(0.0);
            else
                output = static_cast<T>(sqlite3_column_double(m_handle, m_colIdx));

            m_colIdx++;
        }
        else if constexpr (std::is_base_of_v<Row, T>)
        {
            output.unmarshal(*this);
        }
    }

public:
    PreparedStatement() = delete;
    PreparedStatement(const PreparedStatement&) = delete;
    PreparedStatement &operator=(const PreparedStatement&) = delete;

    PreparedStatement(PreparedStatement &&other) noexcept :
        m_handle{other.m_handle},
        m_state{other.m_state},
        m_colIdx{other.m_colIdx},
        m_numCols{other.m_numCols}
    {
        other.m_handle = nullptr;
        other.m_state = State::NotReady;
    }

    PreparedStatement &operator=(PreparedStatement &&other) noexcept
    {
        if (this != &other)
        {
            if (m_handle != nullptr)
                sqlite3_finalize(m_handle);

            m_handle = other.m_handle;
            m_state = other.m_state;
            m_colIdx = other.m_colIdx;
            m_numCols = other.m_numCols;

            other.m_handle = nullptr;
            other.m_state = State::NotReady;
        }

        return *this;
    }

private:
    /// SQLite statement handle
    sqlite3_stmt *m_handle;

    /// Current state of the prepared statement
    State m_state;

    /// Current column index, if a result set is being read
    int m_colIdx;

    /// Number of columns in the result set of the last query
    int m_numCols;
};

// Stream operators
template <typename T>
PreparedStatement &operator<<(PreparedStatement &stmt, const T &input)
{
    stmt.read(input);
    return stmt;
}

template <typename T>
PreparedStatement &operator>>(PreparedStatement &stmt, T &output)
{
    stmt.write(output);
    return stmt;
}

}

#endif // _SQLITE_PREPARED_STATEMENT_H_

